package edu.uw.cs.cse461.sp12.OS;

import java.io.IOException;
import java.net.Socket;
import java.util.Random;

import org.json.JSONException;
import org.json.JSONObject;

import edu.uw.cs.cse461.sp12.util.Log;
import edu.uw.cs.cse461.sp12.util.TCPMessageHandler;

/**
 * Implements a Socket to use in sending remote RPC invocations.  (It must engage
 * in the RPC handshake before sending the invocation request.)
 * @author zahorjan
 *
 */
public class RPCCallerSocket extends Socket {
	// This variable is part of the android Log.x idiom, as in Log.v(TAG, "some debugging log message")
	// You can use Log.x in console apps as well.
	private static final String TAG = "RPCCallerSocket";
	
	private String mRemoteHost;
	
	private Random rand;
	
	/**
	 * Create a socket for sending RPC invocations, connecting it to the specified remote ip and port.
	 * @param hostname In Project 4, it's intended to be the string name of the remote system.  In Project 3, it's not terribly meaningful - repeat the ip.
	 * @param ip  Remote system IP address.
	 * @param port Remote RPC service's port.
	 * @throws IOException
	 */
	public RPCCallerSocket(String hostname, String ip, String port) throws IOException {
		super(ip, Integer.parseInt(port));

		mRemoteHost = hostname;

		// An rpc timeout value is specified in the config file.  You should use that one, not this literal.
		int rpcTimeout = Integer.parseInt(OS.config().getProperty("rpc.timeout", "5000"));
		this.setSoTimeout(rpcTimeout);

		rand = new Random();
	}
	
	/**
	 * Close this socket.
	 */
	@Override
	public void close() throws IOException {
		super.close();
	}
	
	/**
	 * Returns the name of the remote host to which this socket is connected (as specified in the constructor call).
	 * Useful in Project 4.
	 */
	public String remotehost() {
		return mRemoteHost;
	}

	/**
	 * Causes a remote call to the service/method names by the arguments.
	 * @param service Name of the remote service (or application)
	 * @param method Method of that service to invoke
	 * @param userRequest Call arguments
	 * @return
	 */
	public JSONObject invoke(String service, String method, JSONObject userRequest) throws IOException {
		TCPMessageHandler msgHandler = new TCPMessageHandler(this);
		
		// Do initial handshake
		try {
			JSONObject handshakeRequest = createEmptyRequest("control");
			handshakeRequest.put("action", "connect");
			msgHandler.sendMessage(handshakeRequest);
			
			JSONObject handshakeResponse = msgHandler.readMessageAsJSONObject();
			Log.d(TAG, "Received Handshake Response: " + handshakeResponse.toString());
			if (handshakeResponse.getInt("callid") == handshakeRequest.getInt("id")) {

				if(handshakeResponse.getString("type").equals("ERROR")){
					Log.d(TAG, "Received Error: " + handshakeResponse.toString());
					throw new RuntimeException(handshakeResponse.getString("msg"));
				} else if (handshakeResponse.getString("type").equals("OK")){
					// Handshake succeeded.  Send invocation
					JSONObject invokeRequest = createEmptyRequest("invoke");
					invokeRequest.put("app", service);
					invokeRequest.put("args", userRequest);
					invokeRequest.put("method", method);
					msgHandler.sendMessage(invokeRequest);
					
					JSONObject invokeResponse = msgHandler.readMessageAsJSONObject();
					Log.d(TAG, "Received Invoke Response: " + invokeResponse.toString());
					
					if (invokeResponse.getInt("callid") == invokeRequest.getInt("id")) {
						if(invokeResponse.getString("type").equals("ERROR")){
							Log.d(TAG, "Received Error: " + invokeResponse.toString());
							throw new RuntimeException(invokeResponse.getString("message"));
						}  else if (invokeResponse.getString("type").equals("OK")){
							return invokeResponse.getJSONObject("value");
						} else {
							Log.d(TAG, "Unexpected response type: " + invokeResponse.getString("type"));
							throw new RuntimeException("Unexpected response type: " + invokeResponse.getString("type"));
						}
					} else {
						throw new RuntimeException("callid " + invokeResponse.getInt("callid") + " != " + invokeRequest.getInt("id"));
					}
				} else {
					Log.d(TAG, "Unexpected response type: " + handshakeResponse.getString("type"));
					throw new RuntimeException("Unexpected response type: " + handshakeResponse.getString("type"));
				}

			} else {
				throw new RuntimeException("callid " + handshakeResponse.getInt("callid") + " != " + handshakeRequest.getInt("id"));
			}
		} catch (JSONException e) {
			throw new RuntimeException("Unexpected JSONException: " + e.getMessage(), e);
		}
	}
	
	
	
	private JSONObject createEmptyRequest(String type){
		JSONObject request = new JSONObject();
		try{
			request.put("id", rand.nextInt());
			request.put("host", mRemoteHost);
			request.put("type", type);
		} catch (JSONException e){
			// Shouldn't happen
		}
		return request;
	}
	
	
}
